package fr.pud.client.view.jsuggestfield;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.IllegalComponentStateException;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Vector;

import javax.swing.JDialog;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;

/**
 * Provides a text-field that makes suggestions using a provided data-vector.
 * You might have seen this on Google (tm), this is the Java implementation.
 * @author David von Ah
 * @version 0.5
 */
public class JSuggestField extends JTextField {
    /**
     * Inner class providing the independent matcher-thread. This thread can be
     * interrupted, so it won't process older requests while there's already a
     * new one.
     */
    private class InterruptableMatcher extends Thread {
        /** flag used to stop the thread */
        private volatile boolean stop;

        /**
         * Standard run method used in threads
         * responsible for the actual search
         */
        @Override
        public void run() {
            try {
                JSuggestField.this.setFont(JSuggestField.this.busy);
                Iterator<String> it = JSuggestField.this.suggestions.iterator();
                String word = JSuggestField.this.getText();
                while (it.hasNext()) {
                    if (this.stop) {
                        return;
                    }
                    // rather than using the entire list, let's rather remove
                    // the words that don't match, thus narrowing
                    // the search and making it faster
                    if (JSuggestField.this.caseSensitive) {
                        if (!JSuggestField.this.suggestMatcher.matches(
                                it.next(), word)) {
                            it.remove();
                        }
                    }
                    else {
                        if (!JSuggestField.this.suggestMatcher.matches(
                                it.next(), word.toLowerCase())) {
                            it.remove();
                        }
                    }
                }
                JSuggestField.this.setFont(JSuggestField.this.regular);
                if (JSuggestField.this.suggestions.size() > 0) {
                    JSuggestField.this.list
                            .setListData(JSuggestField.this.suggestions);
                    JSuggestField.this.list.setSelectedIndex(0);
                    JSuggestField.this.list.ensureIndexIsVisible(0);
                    JSuggestField.this.d.setVisible(true);
                }
                else {
                    JSuggestField.this.d.setVisible(false);
                }
            }
            catch (Exception e) {
                // Despite all precautions, external changes have occurred.
                // Let the new thread handle it...
                return;
            }
        }
    }

    /** unique ID for serialization */
    private static final long          serialVersionUID = 1756202080423312153L;
    /** Dialog used as the drop-down list. */
    private JDialog                    d;
    /** Location of said drop-down list. */
    private Point                      location;
    /** List contained in the drop-down dialog. */
    private JList                      list;
    /**
     * Vectors containing the original data and the filtered data for the
     * suggestions.
     */
    private Vector<String>             data, suggestions;
    /**
     * Separate matcher-thread, prevents the text-field from hanging while the
     * suggestions are beeing prepared.
     */
    private InterruptableMatcher       matcher;
    /**
     * Fonts used to indicate that the text-field is processing the request,
     * i.e. looking for matches
     */
    private Font                       busy, regular;
    /** Needed for the new narrowing search, so we know when to reset the list */
    private String                     lastWord         = "";
    /**
     * The last chosen variable which exists. Needed if user
     * continued to type but didn't press the enter key
     */
    private String                     lastChosenExistingVariable;
    /**
     * Hint that will be displayed if the field is empty
     */
    private String                     hint;
    /** Listeners, fire event when a selection as occured */
    private LinkedList<ActionListener> listeners;
    private SuggestMatcher             suggestMatcher   = new ContainsMatcher();
    private boolean                    caseSensitive    = false;

    /**
     * Create a new JSuggestField.
     * @param owner
     *            Frame containing this JSuggestField
     */
    public JSuggestField(Frame owner) {
        super();
        this.data = new Vector<String>();
        this.suggestions = new Vector<String>();
        this.listeners = new LinkedList<ActionListener>();
        owner.addComponentListener(new ComponentListener() {
            @Override
            public void componentHidden(ComponentEvent e) {
                JSuggestField.this.relocate();
            }

            @Override
            public void componentMoved(ComponentEvent e) {
                JSuggestField.this.relocate();
            }

            @Override
            public void componentResized(ComponentEvent e) {
                JSuggestField.this.relocate();
            }

            @Override
            public void componentShown(ComponentEvent e) {
                JSuggestField.this.relocate();
            }
        });
        owner.addWindowListener(new WindowListener() {
            @Override
            public void windowActivated(WindowEvent e) {
            }

            @Override
            public void windowClosed(WindowEvent e) {
                JSuggestField.this.d.dispose();
            }

            @Override
            public void windowClosing(WindowEvent e) {
                JSuggestField.this.d.dispose();
            }

            @Override
            public void windowDeactivated(WindowEvent e) {
            }

            @Override
            public void windowDeiconified(WindowEvent e) {
            }

            @Override
            public void windowIconified(WindowEvent e) {
                JSuggestField.this.d.setVisible(false);
            }

            @Override
            public void windowOpened(WindowEvent e) {
            }
        });
        this.addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                if (JSuggestField.this.getText()
                        .equals(JSuggestField.this.hint)) {
                    JSuggestField.this.setText("");
                }
                JSuggestField.this.showSuggest();
            }

            @Override
            public void focusLost(FocusEvent e) {
                JSuggestField.this.d.setVisible(false);
                if (JSuggestField.this.getText().equals("")
                        && e.getOppositeComponent() != null
                        && e.getOppositeComponent().getName() != null) {
                    if (!e.getOppositeComponent().getName()
                            .equals("suggestFieldDropdownButton")) {
                        JSuggestField.this.setText(JSuggestField.this.hint);
                    }
                }
                else if (JSuggestField.this.getText().equals("")) {
                    JSuggestField.this.setText(JSuggestField.this.hint);
                }
            }
        });
        this.d = new JDialog(owner);
        this.d.setUndecorated(true);
        this.d.setFocusableWindowState(false);
        this.d.setFocusable(false);
        this.list = new JList();
        this.list.addMouseListener(new MouseListener() {
            private int selected;

            @Override
            public void mouseClicked(MouseEvent e) {
            }

            @Override
            public void mouseEntered(MouseEvent e) {
            }

            @Override
            public void mouseExited(MouseEvent e) {
            }

            @Override
            public void mousePressed(MouseEvent e) {
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (this.selected == JSuggestField.this.list.getSelectedIndex()) {
                    // provide double-click for selecting a suggestion
                    JSuggestField.this.setText(JSuggestField.this.list
                            .getSelectedValue().toString());
                    JSuggestField.this.lastChosenExistingVariable = JSuggestField.this.list
                            .getSelectedValue().toString();
                    JSuggestField.this.fireActionEvent();
                    JSuggestField.this.d.setVisible(false);
                }
                this.selected = JSuggestField.this.list.getSelectedIndex();
            }
        });
        this.d.add(new JScrollPane(this.list,
                ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER));
        this.d.pack();
        this.addKeyListener(new KeyListener() {
            @Override
            public void keyPressed(KeyEvent e) {
                JSuggestField.this.relocate();
            }

            @Override
            public void keyReleased(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                    JSuggestField.this.d.setVisible(false);
                    return;
                }
                else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
                    if (JSuggestField.this.d.isVisible()) {
                        JSuggestField.this.list
                                .setSelectedIndex(JSuggestField.this.list
                                        .getSelectedIndex() + 1);
                        JSuggestField.this.list
                                .ensureIndexIsVisible(JSuggestField.this.list
                                        .getSelectedIndex() + 1);
                        return;
                    }
                    else {
                        JSuggestField.this.showSuggest();
                    }
                }
                else if (e.getKeyCode() == KeyEvent.VK_UP) {
                    JSuggestField.this.list
                            .setSelectedIndex(JSuggestField.this.list
                                    .getSelectedIndex() - 1);
                    JSuggestField.this.list
                            .ensureIndexIsVisible(JSuggestField.this.list
                                    .getSelectedIndex() - 1);
                    return;
                }
                else if (e.getKeyCode() == KeyEvent.VK_ENTER
                        && JSuggestField.this.list.getSelectedIndex() != -1
                        && JSuggestField.this.suggestions.size() > 0) {
                    JSuggestField.this.setText(JSuggestField.this.list
                            .getSelectedValue().toString());
                    JSuggestField.this.lastChosenExistingVariable = JSuggestField.this.list
                            .getSelectedValue().toString();
                    JSuggestField.this.fireActionEvent();
                    JSuggestField.this.d.setVisible(false);
                    return;
                }
                JSuggestField.this.showSuggest();
            }

            @Override
            public void keyTyped(KeyEvent e) {
            }
        });
        this.regular = this.getFont();
        this.busy = new Font(this.getFont().getName(), Font.ITALIC, this
                .getFont().getSize());
    }

    /**
     * Create a new JSuggestField.
     * @param owner
     *            Frame containing this JSuggestField
     * @param data
     *            Available suggestions
     */
    public JSuggestField(Frame owner, Vector<String> data) {
        this(owner);
        this.setSuggestData(data);
    }

    /**
     * Adds a listener that notifies when a selection has occured
     * @param listener
     *            ActionListener to use
     */
    public void addSelectionListener(ActionListener listener) {
        if (listener != null) {
            this.listeners.add(listener);
        }
    }

    /**
     * Use ActionListener to notify on changes
     * so we don't have to create an extra event
     */
    private void fireActionEvent() {
        ActionEvent event = new ActionEvent(this, 0, this.getText());
        for (ActionListener listener : this.listeners) {
            listener.actionPerformed(event);
        }
    }

    /**
     * Get the hint that will be displayed when the field is empty
     * @return The hint of null if none was defined
     */
    public String getHint() {
        return this.hint;
    }

    /**
     * Returns the selected value in the drop down list
     * @return selected value from the user or null if the entered value does
     *         not
     *         exist
     */
    public String getLastChosenExistingVariable() {
        return this.lastChosenExistingVariable;
    }

    /**
     * Get all words that are available for suggestion.
     * @return Vector containing Strings
     */
    @SuppressWarnings("unchecked")
    public Vector<String> getSuggestData() {
        return (Vector<String>) this.data.clone();
    }

    /**
     * Force the suggestions to be hidden (Useful for buttons, e.g. to use
     * JSuggestionField like a ComboBox)
     */
    public void hideSuggest() {
        this.d.setVisible(false);
    }

    public boolean isCaseSensitive() {
        return this.caseSensitive;
    }

    /**
     * @return boolean Visibility of the suggestion window
     */
    public boolean isSuggestVisible() {
        return this.d.isVisible();
    }

    /**
     * Place the suggestion window under the JTextField.
     */
    private void relocate() {
        try {
            this.location = this.getLocationOnScreen();
            this.location.y += this.getHeight();
            this.d.setLocation(this.location);
        }
        catch (IllegalComponentStateException e) {
            return; // might happen on window creation
        }
    }

    /**
     * Removes the Listener
     * @param listener
     *            ActionListener to remove
     */
    public void removeSelectionListener(ActionListener listener) {
        this.listeners.remove(listener);
    }

    public void setCaseSensitive(boolean caseSensitive) {
        this.caseSensitive = caseSensitive;
    }

    /**
     * Set a text that will be displayed when the field is empty
     * @param hint
     *            Hint such as "Search..."
     */
    public void setHint(String hint) {
        this.hint = hint;
    }

    /**
     * Set maximum size for the drop-down that will appear.
     * @param size
     *            Maximum size of the drop-down list
     */
    public void setMaximumSuggestSize(Dimension size) {
        this.d.setMaximumSize(size);
    }

    /**
     * Set minimum size for the drop-down that will appear.
     * @param size
     *            Minimum size of the drop-down list
     */
    public void setMinimumSuggestSize(Dimension size) {
        this.d.setMinimumSize(size);
    }

    /**
     * Set preferred size for the drop-down that will appear.
     * @param size
     *            Preferred size of the drop-down list
     */
    public void setPreferredSuggestSize(Dimension size) {
        this.d.setPreferredSize(size);
    }

    /**
     * Sets new data used to suggest similar words.
     * @param data
     *            Vector containing available words
     * @return success, true unless the data-vector was null
     */
    public boolean setSuggestData(Vector<String> data) {
        if (data == null) {
            return false;
        }
        // Collections.sort(data);
        this.data = data;
        this.list.setListData(data);
        return true;
    }

    /**
     * Determine how the suggestions are generated.
     * Default is the simple {@link ContainsMatcher}
     * @param suggestMatcher
     *            matcher that determines if a data word may be suggested for
     *            the current
     *            search word.
     */
    public void setSuggestMatcher(SuggestMatcher suggestMatcher) {
        this.suggestMatcher = suggestMatcher;
    }

    /**
     * Force the suggestions to be displayed (Useful for buttons
     * e.g. for using JSuggestionField like a ComboBox)
     */
    public void showSuggest() {
        if (!this.getText().toLowerCase().contains(this.lastWord.toLowerCase())) {
            this.suggestions.clear();
        }
        if (this.suggestions.isEmpty()) {
            this.suggestions.addAll(this.data);
        }
        if (this.matcher != null) {
            this.matcher.stop = true;
        }
        this.matcher = new InterruptableMatcher();
        // matcher.start();
        SwingUtilities.invokeLater(this.matcher);
        this.lastWord = this.getText();
        this.relocate();
    }
}
